
const path = require('path');
const fs = require('fs');
const crypto = require("crypto");
const urllib = require("url");
const Koa = require('koa');
const koaBodyParser = require('koa-bodyparser')

class MockServer {
    constructor(configFile) {
        this.config = JSON.parse(fs.readFileSync(configFile));
        this.config.igoreUrls = this.config.igoreUrls.map((v, i) => new RegExp(v, "gi"));
        for (var domain in this.config.domains) {
            this.config.domains[domain].regexp = new RegExp(this.config.domains[domain].regexp, "gi")
        }
        for (var domain in this.config.rewrite) {
            this.config.rewrite[domain].domain.regexp = new RegExp(this.config.rewrite[domain].domain.regexp, "gi");
        }

        this.domainCache = {};
        this.domainRewriteCache = {};
        this.orgDomainRewriteCache = {};

        fs.mkdirSync(this.config.cache.file, { recursive: true })
    }

    md5(data) {

        var buf = Buffer.from(data);
        var str = buf.toString("binary");
        // var str = Buffer.from("binary");
        return crypto.createHash("md5").update(str).digest("hex");
    }
    clearUrl(url) {
        this.config.igoreUrls.forEach((v, i) => {
            url = url.replace(v, '');
        })
        return url;
    }

    filterDomain(domain) {
        if (this.domainCache[domain]) {
            return this.domainCache[domain];
        }

        for (var key in this.config.domains) {
            if (domain.match(this.config.domains[key].regexp)) {
                this.domainCache[domain] = true;
                return true;
            }
        }

        this.domainCache[domain] = false;
        return this.domainCache[domain];
    }

    rewriteDomain(orgdomain) {
        if (this.domainRewriteCache[orgdomain]) {
            return this.domainRewriteCache[orgdomain];
        }

        for (var key in this.config.rewrite) {
            if (orgdomain.match(this.config.rewrite[key].domain.regexp)) {
                let rewriteDomain = this.domainRewriteCache[orgdomain] = orgdomain.replace(this.config.rewrite[key].domain.regexp, this.config.rewrite[key].domain.to);
                this.orgDomainRewriteCache[rewriteDomain] = orgdomain;
                return rewriteDomain;
            }
        }
       
        //没有找到，是什么返回什么吧，并缓存起来加速
        return this.domainRewriteCache[orgdomain] = orgdomain;
    }

    replaceBody(domain, body) {
        if(!body){
            return body;
        }

        for (var key in this.config.rewrite) {
            if (domain.match(this.config.rewrite[key].domain.regexp)) {
                let bodyConfig = this.config.rewrite[key].body;
                for (let obj of bodyConfig.replace) {
                    if (!obj.fromRegexp) {
                        obj.fromRegexp = new RegExp(obj.from, "gi");
                    }
                    body = body.replace(obj.fromRegexp, obj.to);
                }

            }
        }
        return body;
    }

    startHttpServer() {
        const app = new Koa();
        app.use(koaBodyParser());
        app.use(async ctx => {
            let response = await this.processHttpRequest(ctx);
            let header = response.header;
            delete response.header;
            Object.assign(ctx.response, response);
            ctx.response.set(header);

        });

        app.listen(this.config.httpServer.port, this.config.httpServer.hostname);
    }

    defaultResponse() {
        return {
            statusCode: 200,
            body: "hello world"
        };
    }

    readLocalResponse(orgHostname, filePath) {
        let localResponseData = fs.readFileSync(filePath);

        let localResponse = JSON.parse(fs.readFileSync(filePath + "_info.json"));
        if (localResponse.header["Set-Cookie"]) {
            let setCookies = localResponse.header["Set-Cookie"];
            // session-token-client=ok; domain=dangjian.chinamobile.com; path=/;
            let rgx = /domain=(.+?);/gi
            let objThis = this;
            if(typeof setCookies === 'string'){
                setCookies = setCookies.replace(rgx,function($0,domain){
                    return 'domain=' + objThis.replaceBody(orgHostname,domain) + ';'
                });
            }
            else if(setCookies.map){
                setCookies = setCookies.map((v,i)=>{
                    return v.replace(rgx,function($0,domain){
                        return 'domain=' + objThis.replaceBody(orgHostname,domain) + ';'
                    });
                })
            }
        
            

            localResponse.header["Set-Cookie"] = setCookies;
        }

        if (localResponse.statusCode == 302 || localResponse.statusCode == 301) {
            if (localResponse.header["Location"]) {
                localResponse.header["Location"] = this.replaceBody(orgHostname, localResponse.header["Location"]);
            }
        }
        else {
            let ContentType = localResponse.header["Content-Type"];
            if (ContentType && (
                (
                    ContentType.indexOf("text") != -1)
                ||
                (ContentType.indexOf("javascript") != -1)
                ||
                (ContentType.indexOf("json") != -1)
                ||
                (ContentType.indexOf("css") != -1)
            )) {

                let bodyString = localResponseData.toString('utf8');
                bodyString = this.replaceBody(orgHostname, bodyString);
                localResponseData = Buffer.from(bodyString, "utf8");
            }
            localResponse.body = localResponseData;
            localResponse.header["Content-Length"] = localResponseData.length;
        }

        localResponse.header['X-Proxy-By'] = 'AnyProxy-local';

        return localResponse;


    }

    async processHttpRequest(ctx) {

        let domain = ctx.hostname;
        domain = this.rewriteDomain(domain);
        if (!this.filterDomain(domain)) {
            return this.defaultResponse();
        }

        let method = ctx.method;
        let url_extra_md5 = '';
        let body_extra_md5 = '';

        let urlinfo = new urllib.URL(ctx.href);
        if (urlinfo.search != '') {
            let urlSearch = this.clearUrl(urlinfo.search);
            url_extra_md5 = this.md5(urlSearch);
        }

        let pathName = urlinfo.pathname;
        if (pathName.endsWith('/')) {
            pathName += "default.html";
        }

        switch (method) {
            case "PUT":
            case "POST":
                {
                    if (ctx.headers["content-type"] && ctx.headers["content-type"].indexOf("x-www-form-urlencoded") != -1) { // Content-Type:'application/x-www-form-urlencoded; charset=UTF-8'
                        let rawBody = ctx.request.rawBody;
                        if (rawBody) {
                            let requestData = this.clearUrl(ctx.request.rawBody);
                            body_extra_md5 = this.md5(requestData);
                        }
                    }
                }
                break;
            default:
                break;
        }

        let filePath = path.join(this.config.cache.file, domain, method.toLowerCase(), pathName);
        let pathPath = path.dirname(filePath);
        let fileName = path.basename(filePath);
        let extname = path.extname(filePath);
        if (extname == '') {
            pathPath = filePath;
            fileName = "default";
            extname = ".html";
        }
        let fileNameWithoutExt = fileName.substr(fileName, fileName.length - extname.length);

        filePath = path.join(pathPath, fileNameWithoutExt + (url_extra_md5 != '' ? '_' + url_extra_md5 : '') + (body_extra_md5 != '' ? '_' + body_extra_md5 : '') + extname);
        let filebasePath = path.join(pathPath, fileNameWithoutExt + (body_extra_md5 != '' ? '_' + body_extra_md5 : '') + extname);

        let cacheFile = '';
        if (fs.existsSync(filePath)) {
            cacheFile = filePath;
        } else if (fs.existsSync(filebasePath)) {
            cacheFile = filebasePath;
        }
        else {
            let cacheFiles = fs.readdirSync(pathPath);
            let filelikeRegexp = new RegExp(fileNameWithoutExt + '.*' + extname + "$", "gi");

            let cacheFilename = cacheFiles.find((v, i) => v.match(filelikeRegexp));
            if (cacheFilename) {
                cacheFile = path.join(pathPath, cacheFilename);
            }
        }

        if (cacheFile != '') {
            let orgHostname = ctx.hostname;
            let localResponse = this.readLocalResponse(orgHostname, cacheFile)

            localResponse.status = localResponse.statusCode;
            return localResponse;
        }
        else {
            let path

            return this.defaultResponse();
        }
    }

    beforeSendRequest(requestDetail) {
        let urlinfo = new urllib.URL(requestDetail.url);
        if (urlinfo.searchParams.get('clearcache') == "all") {
            fs.rmdirSync(this.config.cache.file, { force: true, recursive: true });
        }


        let domain = requestDetail.requestOptions.hostname;

        // domain = this.rewriteDomain(domain);

        if (!this.filterDomain(domain)) {
            return null;
        }

        let method = requestDetail.requestOptions.method;

        let url_extra_md5 = '';
        let body_extra_md5 = '';


        if (urlinfo.search != '') {
            let urlSearch = this.clearUrl(urlinfo.search);
            url_extra_md5 = this.md5(urlSearch);
        }

        let pathName = urlinfo.pathname;
        if (pathName.endsWith('/')) {
            pathName += "default.html";
        }

        switch (method) {
            case "PUT":
            case "POST":
                {

                    if (requestDetail.requestOptions.headers["Content-Type"] && requestDetail.requestOptions.headers["Content-Type"].indexOf("x-www-form-urlencoded") != -1) { // Content-Type:'application/x-www-form-urlencoded; charset=UTF-8'
                        let rawBody = requestDetail.requestData.toString();
                        if (rawBody) {
                            rawBody = this.clearUrl(rawBody);
                            body_extra_md5 = this.md5(rawBody);
                        }
                    }
                }
                break;
            default:
                break;
        }

        let filePath = path.join(this.config.cache.file, domain, method.toLowerCase(), pathName);
        let pathPath = path.dirname(filePath);
        let fileName = path.basename(filePath);
        let extname = path.extname(filePath);
        if (extname == '') {
            pathPath = filePath;
            fileName = "default";
            extname = ".html";
        }
        let fileNameWithoutExt = fileName.substr(fileName, fileName.length - extname.length);

        filePath = path.join(pathPath, fileNameWithoutExt + (url_extra_md5 != '' ? '_' + url_extra_md5 : '') + (body_extra_md5 != '' ? '_' + body_extra_md5 : '') + extname);

        if (fs.existsSync(filePath)) {
            let orgHostname = requestDetail.requestOptions.hostname;
            let localResponse = this.readLocalResponse(orgHostname, filePath)
            return {
                response: localResponse
            };
        }
        else {
            requestDetail.requestOptions.saveOptions = { filePath, pathPath };
            const newRequestOptions = requestDetail.requestOptions;
            //去除缓存等
            this.config.removeRequestHeaders.forEach((v, i) => {
                delete newRequestOptions.headers[v];
            });
            return {
                requestOptions: newRequestOptions
            };

        }


    }

    beforeSendResponse(requestDetail, responseDetail) {

        let saveOptions = requestDetail.requestOptions.saveOptions;
        if (!saveOptions) {
            return null;
        }

        let filePath = saveOptions.filePath;
        let pathPath = saveOptions.pathPath;

        if (!fs.existsSync(pathPath)) {
            fs.mkdirSync(pathPath, { recursive: true });
        }


        fs.writeFileSync(filePath, responseDetail.response.body, { encoding: "binary" });
        const newResponse = responseDetail.response;

        this.config.removeResponseHeaders.forEach((v, i) => {
            delete newResponse.header[v];
        })
        Object.assign(newResponse.header, this.config.addResponseHeaders);

        let responseInfo = { statusCode: responseDetail.response.statusCode, header: newResponse.header };
        fs.writeFileSync(filePath + '_info.json', JSON.stringify(responseInfo), { encoding: "utf8" });
        newResponse.header['X-Proxy-By'] = 'AnyProxy-Server';
        return {
            response: newResponse
        };

    }
}


/* 
  sample: 
    modify response header of http://httpbin.org/user-agent
  test:
    curl -I 'http://httpbin.org/user-agent' --proxy http://127.0.0.1:8001
  expected response:
    X-Proxy-By: AnyProxy
*/


module.exports = MockServer